SPB Git

spb/autoscholar

Public
3.2 KB · 84 lines typescript
1import { AppError, audit, transition } from "@autoscholar/core";2import { publicationDecisions, researchRuns } from "@autoscholar/db";3import { eq } from "drizzle-orm";4import { z } from "zod";5import { handler, json, parseBody } from "@/lib/server/api";6import { db } from "@/lib/server/db";7import { requireAdmin } from "@/lib/server/session";89export const runtime = "nodejs";1011/**12 * Authorized override of a *soft* outcome only: moves quality_failed → ready_to_publish → awaiting_approval when every13 * failed gate is in the overridable set. Hard scientific blockers (fabrication, unverified citations, missing PDF,14 * synthetic/simulated artifacts, secrets) can never be overridden.15 */16const NON_OVERRIDABLE = new Set([17  "worker.real",18  "references.verified",19  "pdf.valid",20  "secrets.none",21  "attempt.final",22  "checksums.valid",23  "results.consistency",24  "data.provenance",25  "claims.traced",26  "contract.artifacts",27  "disclosure.present",28]);2930export const POST = handler(async (req, { params }) => {31  const sess = await requireAdmin(req, { mutation: true });32  const { runId } = (await params) as { runId: string };33  const body = await parseBody(req, z.object({ rationale: z.string().min(20).max(2000) }));34  const d = db();35  const [run] = await d.select().from(researchRuns).where(eq(researchRuns.id, runId)).limit(1);36  if (!run) throw new AppError("not_found", "run not found", 404);37  if (run.status !== "quality_failed") throw new AppError("conflict", `run is ${run.status}`, 409);38  const { qualityGateResults } = await import("@autoscholar/db");39  const { desc } = await import("drizzle-orm");40  const gates = await d41    .select()42    .from(qualityGateResults)43    .where(eq(qualityGateResults.runId, runId))44    .orderBy(desc(qualityGateResults.createdAt));45  const latest = gates.filter((g) => g.evaluationId === gates[0]?.evaluationId);46  const failedHard = latest.filter((g) => g.severity === "hard" && g.status === "fail");47  const blocked = failedHard.filter((g) => NON_OVERRIDABLE.has(g.gate));48  if (blocked.length)49    throw new AppError(50      "forbidden",51      `non-overridable gates failed: ${blocked.map((g) => g.gate).join(", ")}`,52      403,53    );54  await d.transaction(async (tx) => {55    await tx.insert(publicationDecisions).values({56      runId,57      evaluationId: latest[0]?.evaluationId ?? null,58      decision: "hold",59      decidedBy: `admin:${sess.userId}`,60      rationale: body.rationale,61      override: true,62      overrideRationale: body.rationale,63      gateSummary: { overriddenGates: failedHard.map((g) => g.gate) },64    });65    await transition(tx, runId, "ready_to_publish", {66      actor: `admin:${sess.userId}`,67      reason: `override: ${body.rationale}`,68      data: { overriddenGates: failedHard.map((g) => g.gate) },69    });70    await transition(tx, runId, "awaiting_approval", {71      actor: `admin:${sess.userId}`,72      reason: "override requires explicit publish action",73    });74    await audit(75      tx,76      { type: "admin", id: sess.userId, ip: sess.ip },77      "quality.override",78      { type: "run", id: runId },79      { rationale: body.rationale, gates: failedHard.map((g) => g.gate) },80    );81  });82  return json({ ok: true, status: "awaiting_approval" });83});84
Files · 92ac91ccf92462b603d014c0325ba84f64d10ea5
Menu

Theme